#include "gtkatspicacheprivate.h"
+#include "gtkatspicontextprivate.h"
+#include "gtkatspirootprivate.h"
+#include "gtkatspiutilsprivate.h"
#include "gtkdebug.h"
#include "a11y/atspi/atspi-cache.h"
+/* Cached item:
+ *
+ * (so): object ref
+ * (so): application ref
+ * (so): parent ref
+ * - parent.role == application ? desktop ref : null ref
+ * i: index in parent, or -1 for transient widgets/menu items
+ * i: child count, or -1 for defunct/menus
+ * as: interfaces
+ * s: name
+ * u: role
+ * s: description
+ * au: state set
+ */
+#define ITEM_SIGNATURE "(so)(so)(so)iiassusau"
+#define GET_ITEMS_SIGNATURE "a(" ITEM_SIGNATURE ")"
+
struct _GtkAtSpiCache
{
GObject parent_instance;
char *cache_path;
GDBusConnection *connection;
- GHashTable *contexts;
+ /* HashTable<str, GtkAtSpiContext> */
+ GHashTable *contexts_by_path;
+
+ /* HashTable<GtkAtSpiContext, str> */
+ GHashTable *contexts_to_path;
};
enum
{
GtkAtSpiCache *self = GTK_AT_SPI_CACHE (gobject);
- g_clear_pointer (&self->contexts, g_hash_table_unref);
+ g_clear_pointer (&self->contexts_to_path, g_hash_table_unref);
+ g_clear_pointer (&self->contexts_by_path, g_hash_table_unref);
g_clear_object (&self->connection);
g_free (self->cache_path);
}
}
+static void
+collect_object (GtkAtSpiCache *self,
+ GVariantBuilder *builder,
+ GtkAtSpiContext *context)
+{
+ g_variant_builder_add (builder, "@(so)", gtk_at_spi_context_to_ref (context));
+
+ GtkAtSpiRoot *root = gtk_at_spi_context_get_root (context);
+ g_variant_builder_add (builder, "@(so)", gtk_at_spi_root_to_ref (root));
+
+ g_variant_builder_add (builder, "@(so)", gtk_at_spi_context_get_parent_ref (context));
+
+ g_variant_builder_add (builder, "i", gtk_at_spi_context_get_index_in_parent (context));
+ g_variant_builder_add (builder, "i", gtk_at_spi_context_get_child_count (context));
+
+ g_variant_builder_add (builder, "@as", gtk_at_spi_context_get_interfaces (context));
+
+ char *name = gtk_at_context_get_name (GTK_AT_CONTEXT (context));
+ g_variant_builder_add (builder, "s", name ? name : "");
+ g_free (name);
+
+ guint atspi_role = gtk_atspi_role_for_context (GTK_AT_CONTEXT (context));
+ g_variant_builder_add (builder, "u", atspi_role);
+
+ char *description = gtk_at_context_get_description (GTK_AT_CONTEXT (context));
+ g_variant_builder_add (builder, "s", description ? description : "");
+ g_free (description);
+
+ g_variant_builder_add (builder, "@au", gtk_at_spi_context_get_states (context));
+}
+
+static void
+collect_cached_objects (GtkAtSpiCache *self,
+ GVariantBuilder *builder)
+{
+ GHashTable *collection = g_hash_table_new (NULL, NULL);
+ GHashTableIter iter;
+ gpointer key_p, value_p;
+
+ /* Serializing the contexts might re-enter, and end up modifying the hash
+ * table, so we take a snapshot here and return the items we have at the
+ * moment of the GetItems() call
+ */
+ g_hash_table_iter_init (&iter, self->contexts_by_path);
+ while (g_hash_table_iter_next (&iter, &key_p, &value_p))
+ g_hash_table_add (collection, value_p);
+
+ g_hash_table_iter_init (&iter, collection);
+ while (g_hash_table_iter_next (&iter, &key_p, &value_p))
+ {
+ g_variant_builder_open (builder, G_VARIANT_TYPE ("(" ITEM_SIGNATURE ")"));
+
+ GtkAtSpiContext *context = value_p;
+
+ collect_object (self, builder, context);
+
+ g_variant_builder_close (builder);
+ }
+
+ g_hash_table_unref (collection);
+}
+
+static void
+emit_add_accessible (GtkAtSpiCache *self,
+ GtkAtSpiContext *context)
+{
+ GVariantBuilder builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("(" ITEM_SIGNATURE ")"));
+
+ collect_object (self, &builder, context);
+
+ g_dbus_connection_emit_signal (self->connection,
+ NULL,
+ self->cache_path,
+ "org.a11y.atspi.Cache",
+ "AddAccessible",
+ g_variant_builder_end (&builder),
+ NULL);
+}
+
+static void
+emit_remove_accessible (GtkAtSpiCache *self,
+ GVariant *ref)
+{
+ g_dbus_connection_emit_signal (self->connection,
+ NULL,
+ self->cache_path,
+ "org.a11y.atspi.Cache",
+ "RemoveAccessible",
+ ref,
+ NULL);
+}
+
static void
handle_cache_method (GDBusConnection *connection,
const gchar *sender,
GDBusMethodInvocation *invocation,
gpointer user_data)
{
+ GtkAtSpiCache *self = user_data;
+
GTK_NOTE (A11Y,
g_message ("[Cache] Method '%s' on interface '%s' for object '%s' from '%s'\n",
method_name, interface_name, object_path, sender));
+
+ if (g_strcmp0 (method_name, "GetItems") == 0)
+ {
+ GVariantBuilder builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("(" GET_ITEMS_SIGNATURE ")"));
+
+ g_variant_builder_open (&builder, G_VARIANT_TYPE (GET_ITEMS_SIGNATURE));
+ collect_cached_objects (self, &builder);
+ g_variant_builder_close (&builder);
+
+ g_dbus_method_invocation_return_value (invocation, g_variant_builder_end (&builder));
+ }
}
static GVariant *
static void
gtk_at_spi_cache_init (GtkAtSpiCache *self)
{
- self->contexts = g_hash_table_new_full (g_str_hash, g_str_equal,
- g_free,
- NULL);
+ self->contexts_by_path = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free,
+ NULL);
+ self->contexts_to_path = g_hash_table_new (NULL, NULL);
}
GtkAtSpiCache *
NULL);
}
+static void
+context_weak_unref (gpointer data,
+ GObject *stale_context)
+{
+ GtkAtSpiCache *self = data;
+
+ const char *path = g_hash_table_lookup (self->contexts_to_path, stale_context);
+ if (path == NULL)
+ return;
+
+ /* By the time we get here, the context has already been dropped,
+ * so we need to generate the reference ourselves
+ */
+ emit_remove_accessible (self, g_variant_new ("(so)",
+ g_dbus_connection_get_unique_name (self->connection),
+ path));
+
+ GTK_NOTE (A11Y, g_message ("Removing stale context '%s' from cache", path));
+
+ g_hash_table_remove (self->contexts_by_path, path);
+ g_hash_table_remove (self->contexts_to_path, stale_context);
+}
+
void
-gtk_at_spi_cache_add_context (GtkAtSpiCache *self,
- const char *path,
- GtkATContext *context)
+gtk_at_spi_cache_add_context (GtkAtSpiCache *self,
+ GtkAtSpiContext *context)
{
g_return_if_fail (GTK_IS_AT_SPI_CACHE (self));
- g_return_if_fail (path != NULL);
- g_return_if_fail (GTK_IS_AT_CONTEXT (context));
+ g_return_if_fail (GTK_IS_AT_SPI_CONTEXT (context));
+
+ const char *path = gtk_at_spi_context_get_context_path (context);
+ if (path == NULL)
+ return;
- if (g_hash_table_contains (self->contexts, path))
+ if (g_hash_table_contains (self->contexts_by_path, path))
return;
- g_hash_table_insert (self->contexts, g_strdup (path), context);
+ g_object_weak_ref (G_OBJECT (context), context_weak_unref, self);
+
+ char *path_key = g_strdup (path);
+ g_hash_table_insert (self->contexts_by_path, path_key, context);
+ g_hash_table_insert (self->contexts_to_path, context, path_key);
+
+ emit_add_accessible (self, context);
+
+ GTK_NOTE (A11Y, g_message ("Adding context '%s' to cache", path_key));
}
-GtkATContext *
-gtk_at_spi_cache_get_context (GtkAtSpiCache *self,
- const char *path)
+void
+gtk_at_spi_cache_remove_context (GtkAtSpiCache *self,
+ GtkAtSpiContext *context)
{
- g_return_val_if_fail (GTK_IS_AT_SPI_CACHE (self), NULL);
- g_return_val_if_fail (path != NULL, NULL);
+ g_return_if_fail (GTK_IS_AT_SPI_CACHE (self));
+ g_return_if_fail (GTK_IS_AT_SPI_CONTEXT (context));
+
+ const char *path = gtk_at_spi_context_get_context_path (context);
+ if (!g_hash_table_contains (self->contexts_by_path, path))
+ return;
+
+ emit_remove_accessible (self, gtk_at_spi_context_to_ref (context));
+
+ g_object_weak_unref (G_OBJECT (context), context_weak_unref, self);
+
+ /* The order is important: the value in contexts_by_path is the
+ * key in contexts_to_path
+ */
+ g_hash_table_remove (self->contexts_to_path, context);
+ g_hash_table_remove (self->contexts_by_path, path);
- return g_hash_table_lookup (self->contexts, path);
+ GTK_NOTE (A11Y, g_message ("Removing context '%s' from cache", path));
}
}
else if (g_strcmp0 (method_name, "GetIndexInParent") == 0)
{
- GtkAccessible *accessible = gtk_at_context_get_accessible (GTK_AT_CONTEXT (self));
- int idx;
-
- if (GTK_IS_ROOT (accessible))
- idx = get_index_in_toplevels (GTK_WIDGET (accessible));
- else if (GTK_IS_STACK_PAGE (accessible))
- idx = get_index_in_parent (gtk_stack_page_get_child (GTK_STACK_PAGE (accessible)));
- else if (GTK_IS_STACK (gtk_widget_get_parent (GTK_WIDGET (accessible))))
- idx = 1;
- else
- idx = get_index_in_parent (GTK_WIDGET (accessible));
+ int idx = gtk_at_spi_context_get_index_in_parent (self);
if (idx == -1)
g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "Not found");
else if (g_strcmp0 (property_name, "Parent") == 0)
res = get_parent_context_ref (accessible);
else if (g_strcmp0 (property_name, "ChildCount") == 0)
- {
- int n_children = 0;
-
- if (GTK_IS_WIDGET (accessible))
- {
- GtkWidget *child;
-
- for (child = gtk_widget_get_first_child (GTK_WIDGET (accessible));
- child;
- child = gtk_widget_get_next_sibling (child))
- {
- if (!gtk_accessible_should_present (GTK_ACCESSIBLE (child)))
- continue;
-
- n_children++;
- }
- }
- else if (GTK_IS_STACK_PAGE (accessible))
- {
- n_children = 1;
- }
-
- res = g_variant_new_int32 (n_children);
- }
+ res = g_variant_new_int32 (gtk_at_spi_context_get_child_count (self));
else
g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
"Unknown property '%s'", property_name);
self);
gtk_at_spi_context_register_object (self);
- gtk_at_spi_root_queue_register (self->root);
+ gtk_at_spi_root_queue_register (self->root, self);
}
static void
/* Notify ATs that the accessible object is going away */
emit_defunct (self);
+ gtk_at_spi_root_unregister (self->root, self);
gtk_atspi_disconnect_text_signals (accessible);
gtk_atspi_disconnect_selection_signals (accessible);
return self->root;
}
+
+int
+gtk_at_spi_context_get_index_in_parent (GtkAtSpiContext *self)
+{
+ g_return_val_if_fail (GTK_IS_AT_SPI_CONTEXT (self), -1);
+
+ GtkAccessible *accessible = gtk_at_context_get_accessible (GTK_AT_CONTEXT (self));
+ int idx;
+
+ if (GTK_IS_ROOT (accessible))
+ idx = get_index_in_toplevels (GTK_WIDGET (accessible));
+ else if (GTK_IS_STACK_PAGE (accessible))
+ idx = get_index_in_parent (gtk_stack_page_get_child (GTK_STACK_PAGE (accessible)));
+ else if (GTK_IS_STACK (gtk_widget_get_parent (GTK_WIDGET (accessible))))
+ idx = 1;
+ else
+ idx = get_index_in_parent (GTK_WIDGET (accessible));
+
+ return idx;
+}
+
+int
+gtk_at_spi_context_get_child_count (GtkAtSpiContext *self)
+{
+ g_return_val_if_fail (GTK_IS_AT_SPI_CONTEXT (self), -1);
+
+ GtkAccessible *accessible = gtk_at_context_get_accessible (GTK_AT_CONTEXT (self));
+ int n_children = -1;
+
+ if (GTK_IS_WIDGET (accessible))
+ {
+ GtkWidget *child;
+
+ n_children = 0;
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (accessible));
+ child;
+ child = gtk_widget_get_next_sibling (child))
+ {
+ if (!gtk_accessible_should_present (GTK_ACCESSIBLE (child)))
+ continue;
+
+ n_children++;
+ }
+ }
+ else if (GTK_IS_STACK_PAGE (accessible))
+ {
+ n_children = 1;
+ }
+
+ return n_children;
+}
/* }}} */
/* vim:set foldmethod=marker expandtab: */
#include "gtkatspirootprivate.h"
+#include "gtkatspicacheprivate.h"
#include "gtkatspicontextprivate.h"
#include "gtkaccessibleprivate.h"
#include "gtkatspiprivate.h"
gint32 application_id;
guint register_id;
+ GList *queued_contexts;
GtkAtSpiCache *cache;
GListModel *toplevels;
/* Register the cache object */
self->cache = gtk_at_spi_cache_new (self->connection, ATSPI_CACHE_PATH);
+ /* Drain the list of queued GtkAtSpiContexts, and add them to the cache */
+ if (self->queued_contexts != NULL)
+ {
+ for (GList *l = self->queued_contexts; l != NULL; l = l->next)
+ gtk_at_spi_cache_add_context (self->cache, l->data);
+
+ g_clear_pointer (&self->queued_contexts, g_list_free);
+ }
+
self->toplevels = gtk_window_get_toplevels ();
}
* Queues the registration of the root object on the AT-SPI bus.
*/
void
-gtk_at_spi_root_queue_register (GtkAtSpiRoot *self)
+gtk_at_spi_root_queue_register (GtkAtSpiRoot *self,
+ GtkAtSpiContext *context)
{
+ /* The cache is available if the root has finished registering itself; if we
+ * are still waiting for the registration to finish, add the context to a queue
+ */
+ if (self->cache != NULL)
+ {
+ gtk_at_spi_cache_add_context (self->cache, context);
+ return;
+ }
+ else
+ {
+ if (g_list_find (self->queued_contexts, context) == NULL)
+ self->queued_contexts = g_list_prepend (self->queued_contexts, context);
+ }
+
/* Ignore multiple registration requests while one is already in flight */
if (self->register_id != 0)
return;
- /* The cache is only available once the registration succeeds */
- if (self->cache != NULL)
- return;
-
self->register_id = g_idle_add (root_register, self);
g_source_set_name_by_id (self->register_id, "[gtk] ATSPI root registration");
}
+void
+gtk_at_spi_root_unregister (GtkAtSpiRoot *self,
+ GtkAtSpiContext *context)
+{
+ if (self->queued_contexts != NULL)
+ self->queued_contexts = g_list_remove (self->queued_contexts, context);
+
+ if (self->cache != NULL)
+ gtk_at_spi_cache_remove_context (self->cache, context);
+}
+
static void
gtk_at_spi_root_constructed (GObject *gobject)
{